package mathematics.geometry;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import utils.AlignedBox3D;
import utils.Point3D;
import utils.Ray3D;
import utils.Vector3D;

public abstract class Geometry extends ArrayList<Point3D> {

	/**
	 * 
	 */
	private static final long serialVersionUID = -8345281281449556510L;
	private Point3D position;
	private AlignedBox3D boundingBox;
	private boolean isLoop = false;
	
	public Geometry(ArrayList<Point3D> points)
	{
		this();
		this.addAll(points);
	}
	
	public Geometry() {
		this.position = new Point3D();
		this.boundingBox = new AlignedBox3D();
	}

	
	public Point3D getPosition() {
		return position;
	}

	public void setPosition(Point3D position) {
		this.position = position;
	}
	public AlignedBox3D getBoundingBox()
	{
		return this.boundingBox;
	}
	public boolean isLoop() {
		return isLoop;
	}
	public void setLoop(boolean isLoop) {
		this.isLoop = isLoop;
	}
	
	@Override
	public boolean add(Point3D point)
	{
		boolean result = super.add(point);
		boundingBox.bound(this);
		return result;
	}
	@Override
	public boolean addAll(Collection<? extends Point3D> points)
	{
		boolean result = super.addAll(points);
		boundingBox.bound(this);
		return result;
	}
	@Override 
	public void clear()
	{
		super.clear();
		boundingBox.bound(this);
	}
	
	public boolean isIntersectedBy(Ray3D ray)
	{
		if(this.isEmpty())
		{
			return false;
		}
		Point3D intersection = new Point3D();
		Vector3D normalAtIntersection = new Vector3D();
		
		if(this.getBoundingBox().intersects(ray, intersection, normalAtIntersection))
		{
			return this.isIntersectedBy(intersection);
		}
		else
		{
			return false;
		}
		
	}
	// Return true if there is a "crossing"
	public boolean isIntersectedBy(Geometry geometry)
	{
		int thisCount = this.size();
		if(!this.isLoop())
		{
			thisCount--;
		}
		
		int geometryCount = geometry.size();
		if(!geometry.isLoop())
		{
			geometryCount--;
		}
		
		for(int thisIndex = 0; thisIndex < thisCount; thisIndex++)
		{
			Point3D A = this.get(thisIndex);
			Point3D B = this.get((thisIndex+1) % this.size());
			
			for(int geometryIndex = 0; geometryIndex < geometryCount; geometryIndex++)
			{
				
				Point3D C = geometry.get(geometryIndex);
				Point3D D = geometry.get((geometryIndex + 1) % geometry.size());
		
				Vector3D v1 = new Vector3D(B.x() - A.x(), B.y() - A.y());
				Vector3D v2 = new Vector3D(D.x() - C.x(), D.y() - C.y());
				
				double denom = (v1.x() * v2.y() - v1.y() * v2.x());
				if (denom == 0)
				{
					continue;
				}
				
				double t = - ( A.x() * v2.y() - C.x() * v2.y() - v2.x() * A.y() + v2.x() * C.y() ) / denom;
				if (t<0 || t>=1)
				{
					continue;
				}
				
				double u = - (-v1.x() * A.y() + v1.x() * C.y() + v1.y() * A.x() - v1.y() * C.x()) / denom;
					if (u<0 || u>=1)
				{
					continue;
				}
				
				return true;
			}
		}
		return false;
	}
	
	// Return true if there is a "crossing"
	public boolean isIntersectedBy(Geometry geometry, Point3D intersection)
	{
		int thisCount = this.size();
		if(!this.isLoop())
		{
			thisCount--;
		}
		
		int geometryCount = geometry.size();
		if(!geometry.isLoop())
		{
			geometryCount--;
		}
		
		for(int thisIndex = 0; thisIndex < thisCount; thisIndex++)
		{
			Point3D A = this.get(thisIndex);
			Point3D B = this.get((thisIndex+1) % this.size());
			
			for(int geometryIndex = 0; geometryIndex < geometryCount; geometryIndex++)
			{
				
				Point3D C = geometry.get(geometryIndex);
				Point3D D = geometry.get((geometryIndex + 1) % geometry.size());
		
				Vector3D v1 = new Vector3D(B.x() - A.x(), B.y() - A.y());
				Vector3D v2 = new Vector3D(D.x() - C.x(), D.y() - C.y());
				
				double denom = (v1.x() * v2.y() - v1.y() * v2.x());
				if (denom == 0)
				{
					continue;
				}
				
				double t = - ( A.x() * v2.y() - C.x() * v2.y() - v2.x() * A.y() + v2.x() * C.y() ) / denom;
				if (t<0 || t>=1)
				{
					continue;
				}
				
				double u = - (-v1.x() * A.y() + v1.x() * C.y() + v1.y() * A.x() - v1.y() * C.x()) / denom;
					if (u<0 || u>=1)
				{
					continue;
				}
				
				Vector3D vAB = new Vector3D(B.x() - A.x(), B.y() - A.y());
				Vector3D intersectionVector = Vector3D.mult( vAB, t);
				intersection.copy(Point3D.sum(A, intersectionVector));
				return true;
			}
		}
		return false;
	}
	
	// Return true if there is a "crossing"
	public boolean isIntersectedBy(Geometry geometry, ArrayList<Point3D> intersections)
	{
		boolean result = false;
		int thisCount = this.size();
		if(!this.isLoop())
		{
			thisCount--;
		}
		
		int geometryCount = geometry.size();
		if(!geometry.isLoop())
		{
			geometryCount--;
		}
		
		for(int thisIndex = 0; thisIndex < thisCount; thisIndex++)
		{
			Point3D A = this.get(thisIndex);
			Point3D B = this.get((thisIndex+1) % this.size());
			
			for(int geometryIndex = 0; geometryIndex < geometryCount; geometryIndex++)
			{
				
				Point3D C = geometry.get(geometryIndex);
				Point3D D = geometry.get((geometryIndex + 1) % geometry.size());
		
				Vector3D v1 = new Vector3D(B.x() - A.x(), B.y() - A.y());
				Vector3D v2 = new Vector3D(D.x() - C.x(), D.y() - C.y());
				
				double denom = (v1.x() * v2.y() - v1.y() * v2.x());
				if (denom == 0)
				{
					continue;
				}
				
				double t = - ( A.x() * v2.y() - C.x() * v2.y() - v2.x() * A.y() + v2.x() * C.y() ) / denom;
				if (t<0 || t>=1)
				{
					continue;
				}
				
				double u = - (-v1.x() * A.y() + v1.x() * C.y() + v1.y() * A.x() - v1.y() * C.x()) / denom;
					if (u<0 || u>=1)
				{
					continue;
				}
				
				Vector3D vAB = new Vector3D(B.x() - A.x(), B.y() - A.y());
				Vector3D intersectionVector = Vector3D.mult( vAB, t);
				intersections.add(Point3D.sum(A, intersectionVector));
				result = true;
			}
		}
		return result;
	}
	
	public boolean isIntersectedBy(Point3D point)
	{
		Point3D rayPoint = new Point3D(point.x(), point.y());
		Point3D infinitePoint = new Point3D( -1000000000 , -1000000000);

		SegmentGeometry segment = new SegmentGeometry(infinitePoint, rayPoint );
		
		int count = 0;		
		
		int thisCount = this.size();
		if(!this.isLoop())
		{
			thisCount--;
		}
		
		
		for(int i = 0; i < thisCount; i++)
		{
			Point3D A = this.get(i);
			A = Point3D.sum(A, position);

			Point3D B = this.get( (i+1) % this.size() );
			B = Point3D.sum(B, position);
			
			SegmentGeometry AB = new SegmentGeometry(A, B);
			if(segment.isIntersectedBy(AB))
			{
				count++;
			}
		}
		return (count % 2 == 1 );
	}
	
	public boolean isInside(Geometry geometry)
	{
		if(!geometry.isIntersectedBy(this.getCentroid()))
		{
			return false;
		}
		for(Iterator<Point3D> it = geometry.iterator(); it.hasNext(); )
		{
			Point3D point = it.next();
			point = Point3D.sum(point, position);
			if(this.isIntersectedBy(point))
			{
				return false;
			}
		}
		return true;
	}
	
	public Point3D getCentroid()
	{
		double x = 0; 
		double y = 0;
		double area6 = this.getArea() * 6;
		for(int i = 0; i < this.size(); i++)
		{
			int nextIndex = (i + 1) % this.size();
			x += ( this.get(i).x() + this.get(nextIndex).x()) * ( this.get(i).x() * this.get( nextIndex).y() - this.get(nextIndex).x() * this.get(i).y()  );
			y += ( this.get(i).y() + this.get(nextIndex).y()) * ( this.get(i).x() * this.get(nextIndex).y() - this.get(nextIndex).x() * this.get(i).y()  );
		}
	
		Point3D result = new Point3D(x / area6, y / area6);
		result = Point3D.sum(result, position);
		return result;
	}

	public float getArea()
	{
		float area = 0;
		
		for(int i = 0; i < this.size(); i++)
		{
			area += ( this.get(i).x() * this.get( (i + 1) % this.size()).y() - this.get((i + 1) % this.size()).x() * this.get(i).y());
		}

		return area / 2.0f;
	}	

	
	public void offset(float offset)
	{
		ArrayList<Point3D> result = new ArrayList<Point3D>();
		int count = this.size();
		if(!this.isLoop())
		{
			count--;
		}

		SegmentGeometry lastSegment = null;
		for(int index = 0; index < count; index++)
		{
			Point3D A = this.get(index);
			Point3D B = this.get( (index+1) % this.size());
			
			Vector3D vAB = new Vector3D(A,B);
			Vector3D perpendVAB = new Vector3D(-vAB.y(), vAB.x());
			perpendVAB = perpendVAB.normalized();
			perpendVAB = Vector3D.mult(perpendVAB, - offset);
			
			A = Point3D.sum(A, perpendVAB);
			B = Point3D.sum(B, perpendVAB);
	    	SegmentGeometry segment = new SegmentGeometry(A, B);
	    	
			Point3D intersection = new Point3D();
			if(lastSegment != null && lastSegment.isIntersectedBy(segment, intersection ))
			{
				result.remove(result.size()-1);
				result.add(intersection);
			}
			else
			{
				result.add(A);
			}
			result.add(B);
			lastSegment = segment;
		}
		this.clear();
		this.addAll(result);
	}

	
	public void clip(Geometry shape)
	{
		ArrayList<Point3D> result = new ArrayList<Point3D>();

		int clipCount = 0;
		int count = this.size();
		if(!this.isLoop())
		{
			count--;
		}

				
		for(int i = 0; i < count; i++)
		{
			Point3D point = this.get(i);
			Point3D nextPoint = this.get( (i+1) % this.size());
			
			SegmentGeometry segment = new SegmentGeometry(point, nextPoint);
			Point3D intersection = new Point3D();
			if(segment.getA().isInside(shape))
			{
				if(shape.isIntersectedBy(segment, intersection))
				{
					result.add(intersection);
					result.add(segment.getB());
					clipCount++;
				}
			}
			else if(segment.getB().isInside(shape))	
			{
				if(shape.isIntersectedBy(segment, intersection))
				{
					result.add(segment.getA());
					result.add(intersection);
					clipCount++;
				}
			}
			else
			{
				result.addAll(segment);
			}
			
			if(clipCount ==2)
			{
				break;
			}
		}
		this.clear();
		this.addAll(result);
	}
	
	public double processIntersectionLength(SegmentGeometry segment)
	{
		ArrayList<Point3D> points = new ArrayList<Point3D>();
		this.isIntersectedBy(segment, points);
		if(points.size() == 2)
		{
			SegmentGeometry result = new SegmentGeometry(points.get(0), points.get(1));
			return result.getLength();
		}
		return 0;
	}
}
